COMP3141
Software System Design and Implementation (18s1)

Exercise (Week 6)

Table of Contents

Ex04-icon.png

DUE: Sun 15 April 2017 23:59:59

Download the exercise tarball and extract it to a directory in your home directory at CSE. This tarball contains a file, called Ex04.hs, wherein you will do all of your programming.

To test your code, run the following shell commands to open a GHCi session:

$ 3141
newclass starting new subshell for class COMP3141...
$ cabal repl
Resolving dependencies...
Configuring Ex04-1.0...
Preprocessing executable 'Ex04' for Ex04-1.0..
GHCi, version 8.2.2: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Ex04          (Ex04.hs, interpreted)
Ok, one module loaded.
*Ex04> calculate "1 1 +"
...

Note that you will only need to submit Ex04.hs, so only make changes to that file.

Download the exercise tarball and extract it to a directory on your local machine. This tarball contains a file, called Ex04.hs, wherein you will do all of your programming.

To test your code, run the following shell commands to open a GHCi session:

$ stack repl
Configuring GHCi with the following packages: Ex04
Using main module: 1. Package 'Ex04' component exe:Ex04 ...
GHCi, version 8.2.2: http://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Ex04          (Ex04.hs, interpreted)
[2 of 2] Compiling Main          (Main.hs, interpreted)
Ok, two modules loaded.
*Main Ex04> calculate "1 1 +"
...

Note that you will only need to submit Ex04.hs, so only make changes to that file.

Download the Haskell for Mac project and unzip it somewhere on your local machine. Open the project in Haskell for Mac, and begin coding in Ex04.hs.

Note that you will only need to submit Ex04.hs, so only make changes to that file.

1 Reverse Polish Notation

The Reverse Polish Notation is a common way to represent arithmetic expressions, where each operator appears after the operands it works on. Below are a number of examples in a sample GHCi session. Your task in this exercise is to develop a RPN calculator using a variety of monadic functions. You should be able to evaluate the same examples to the same results after you have completed this exercise:

*Ex04> calculate "3 4 +"
Just 7
*Ex04> calculate "3 4 - 5 +"
Just 4
*Ex04> calculate "3 4 2 / *"
Just 6
*Ex04> calculate "3 4 2 / * +"
Nothing

Generally, evaluation of RPN proceeds by looking at each token (number or operator), one at a time. We evaluate a number by pushing it on to a stack, and we evaluate an operator o by popping two numbers y and x from the stack, and pushing o x y back on. The number left on the top of the stack at the end of evaluating is the result of the expression.

1.1 Parsing (1 Mark)

We define a Token as either a number or a binary operator:

data Token = Number Int | Operator (Int -> Int -> Int)

First, we need a way to translate String values into proper calculable expressions, that is, lists of Token. We have already defined a function that will attempt to convert a single word into a single Token, called parseToken:

parseToken :: String -> Maybe Token
parseToken "+" = Just (Operator (+))
parseToken "-" = Just (Operator (-))
parseToken "/" = Just (Operator div)
parseToken "*" = Just (Operator (*))
parseToken str = fmap Number (readMaybe str)

Your first task is to implement the function tokenise:

tokenise :: String -> Maybe [Token]

This function must break the string into words (the built-in words function will be useful), and then attempt to parse each one into a Token, returning a non-Nothing result only if every word can be parsed.

Note that you can (and should) exploit the fact that Maybe is an instance of Monad, Applicative and Functor. This will enable you to make the above function into a short, one-line definition.

1.2 Stack Operations (2 Marks)

Next, in order to evaluate RPN expressions, we need a convenient way to model computations which manipulate a stack, and can possibly fail (for example, if we were to try to pop from an empty stack).

In Haskell, such a computation could be modelled as a function that, given an input stack (of Int), will either fail (returning Nothing) or return Just an output stack with an additional return value.

newtype Calc a = C ([Int] -> Maybe ([Int], a))

Implement the two basic stack operations, push and pop as Calc computations.

pop  :: Calc Int
push :: Int -> Calc ()

1.3 Evaluating (3 Marks)

We have provided instances of Monad, Applicative and Functor for Calc:

instance Functor Calc where
  fmap f (C sa) = C $ \s ->
      case sa s of
        Nothing      -> Nothing
        Just (s', a) -> Just (s', f a)

instance Applicative Calc where
  pure x = C (\s -> Just (s,x))
  C sf <*> C sx = C $ \s ->
      case sf s of
          Nothing     -> Nothing
          Just (s',f) -> case sx s' of
              Nothing      -> Nothing
              Just (s'',x) -> Just (s'', f x)

instance Monad Calc where
  return = pure
  C sa >>= f = C $ \s ->
      case sa s of
          Nothing     -> Nothing
          Just (s',a) -> unwrapCalc (f a) s'
    where unwrapCalc (C a) = a

All three instances make sure that the whole computation will return Nothing if any subcomputation returns Nothing. Furthermore, the Applicative and Monad instances allow us to thread the stack through multiple computations without manually keeping track of this state. For example, the Calc computation that adds the two topmost elements of the stack and pushes the result back to the stack can be defined as:

addTwo :: Calc ()
addTwo = do
  x <- pop
  y <- pop
  push (x + y)

By threading the stack through in the Monad instance here, we can just treat the stack as some implicit state that we manipulate abstractly with Calc computations. This allows us to make code much shorter and cleaner.

Using the above instances for Calc, define a function evaluate:

evaluate :: [Token] -> Calc Int

This should evaluate a list of Token as described above, using the stack operations you defined earlier, finally returning the topmost element of the stack.

1.4 Putting it together (1 Mark)

Lastly, define a function calculate:

calculate :: String -> Maybe Int

That will first attempt to parse the given string with tokenise to get a list of tokens. If that succeeds, it should use evaluate to get a Calc computation corresponding to that list of tokens. Then, it should run that computation starting with an empty stack, returning just the resultant integer from that computation, if any. If the stack has more than one element when calculate runs, it should return the topmost element.

Note that once again you can use the Monad, Applicative and Functor instances of Maybe to succinctly implement this function.

After implementing this function, test it with the examples listed at the beginning of this exercise, and try your own examples also.

2 Peer review (2 Marks)

Now that you've all made some fantastic artworks, we'd like each of you that submitted a picture to review one other student's picture. To do this, just log in to the peer review form here, and answer the questions the form asks you. Make sure you review carefully and kindly, because your review will be posted on the gallery page for that picture, and your identity is revealed to the person who made the image. To check the reviews for your own artworks, you can click here.

3 Submission instructions

You can submit your exercise by typing:

$ give cs3141 Ex04 Ex04.hs

on a CSE terminal, or by using the give web interface. Your file must be named Ex04.hs (case-sensitive!). A dry-run test will partially autotest your solution at submission time. To get full marks, you will need to perform further testing yourself.

2018-06-14 Thu 18:28

Announcements RSS